package svr

import (
	"context"
	"crypto/tls"
	"encoding/base64"
	"errors"
	"gitee.com/yysf-xin/dproxy/pkg/app"
	"gitee.com/yysf-xin/dproxy/pkg/db"
	"gitee.com/yysf-xin/dproxy/pkg/svr/ssl"
	"gitee.com/yysf-xin/dproxy/pkg/tools"
	"golang.org/x/net/proxy"
	"io"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strconv"
	"strings"
	"time"
)

var (
	ProxyErr = errors.New("use http proxy error")
)

var (
	CtxRequest = "request"
)

type ContextVal struct {
	Req  *http.Request
	Conn net.Conn
	Rpy  bool
}

type DialerInfo struct {
	Url *url.URL
}

type ProxyDialer struct {
	Name   string
	Proxy  *db.Proxy
	Dialer func(ctx context.Context, network, address string) (net.Conn, error)
}

func (p *ProxyDialer) String() string {
	return p.Name
}

func (p *ProxyDialer) Start() error {
	return nil
}

func (p *ProxyDialer) Stop() {
}
func basicAuth(username, password string) string {
	auth := username + ":" + password
	return base64.StdEncoding.EncodeToString([]byte(auth))
}
func (p *ProxyDialer) proxyAuth() string {
	if p.Proxy.ProxyURL == nil || p.Proxy.ProxyURL.User == nil {
		return ""
	}
	return "Basic " + basicAuth(p.Proxy.UserName, p.Proxy.Password)
}
func (p *ProxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
	return p.Dialer(ctx, network, address)
}

func NewProxyDialer(i *db.Proxy, forward proxy.Dialer) (p *ProxyDialer, err error) {
	if i.ProxyURL == nil {
		if i.ProxyURL, err = url.Parse(i.URL); err != nil {
			return
		}
		if i.HasAuth || i.UserName != "" {
			i.ProxyURL.User = url.UserPassword(i.UserName, i.Password)
		}
	}
	var dialer proxy.Dialer
	if dialer, err = proxy.FromURL(i.ProxyURL, forward); err == nil {
		if d, ok := dialer.(proxy.ContextDialer); ok {
			p = &ProxyDialer{
				Proxy:  i,
				Dialer: d.DialContext,
			}
		} else {
			p = &ProxyDialer{
				Proxy: i,
				Dialer: func(ctx context.Context, network, address string) (net.Conn, error) {
					return dialer.Dial(network, address)
				},
			}
		}
	} else if i.ProxyURL.Scheme == "http" || i.ProxyURL.Scheme == "https" {
		if i.UserName != "" {
			i.Password = "Basic " + basicAuth(i.UserName, i.Password)
		}
		p = &ProxyDialer{
			Proxy: i,
			Dialer: func(ctx context.Context, network, address string) (conn net.Conn, err error) {
				if forward == nil {
					conn, err = net.Dial(network, i.ProxyURL.Host)
				} else {
					conn, err = forward.(proxy.ContextDialer).DialContext(ctx, network, i.ProxyURL.Host)
				}

				if err == nil {
					if src, ok := ctx.Value(CtxRequest).(*ContextVal); ok {
						src.Rpy = false
						if i.Password != "" {
							src.Req.Header.Set("Proxy-Authorization", i.Password)
						}
						_ = src.Req.Write(conn)
					} else {
						// todo impl socket proxy over http
						err = ProxyErr
					}
				}

				return
			},
		}
		err = nil
	}

	if p != nil {
		p.Name = strconv.Itoa(int(p.Proxy.Id)) + "@PROXY"
	} else if err == nil {
		err = ProxyErr
	}
	return
}

type Director interface {
	Director(*http.Request)
	HasReverse(host string) bool
}

type Proxy struct {
	Proxy *httputil.ReverseProxy
	Tr    *http.Transport
	d     Director
	https *http.Server
	l     listener
}

type listener struct {
	connChan chan net.Conn
}

func (l *listener) Accept() (net.Conn, error) {
	conn, ok := <-l.connChan
	if ok {
		return conn, nil
	} else {
		return nil, http.ErrServerClosed
	}
}

func (l *listener) Close() error {
	return nil
}

func (l *listener) Addr() net.Addr {
	return nil
}

func NewProxy(tr *http.Transport, d Director, cfg *app.Ssl) *Proxy {

	p := &Proxy{Proxy: &httputil.ReverseProxy{
		Director:  d.Director,
		Transport: http.DefaultTransport,
		// todo 添加网络监控
		//ModifyResponse: func(r *http.Response) error {
		//	if strings.Contains(r.Request.Header.Get("Accept"), "application/json") {
		//		body, err := httputil.DumpRequest(r.Request, true)
		//		if err != nil {
		//			return err
		//		}
		//		tools.Log.Println(tools.BytesToStringFast(body))
		//		body, err = httputil.DumpResponse(r, true)
		//		if err != nil {
		//			return err
		//		}
		//		tools.Log.Println(tools.BytesToStringFast(body))
		//	}
		//	return nil
		//},
	}, Tr: tr, d: d}

	if cfg.Cert != "" && cfg.Key != "" {
		ca, err := ssl.NewCa(cfg.Cert, cfg.Key)
		if err == nil {
			p.https = &http.Server{
				Handler: p.Proxy,
				TLSConfig: &tls.Config{
					GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
						return ca.DummyCert(info.ServerName)
					},
				},
			}
			p.l.connChan = make(chan net.Conn)
			go p.https.ServeTLS(&p.l, "", "")
		}
	}

	return p
}
func send200(src io.Writer) {
	_, _ = src.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))
}
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodConnect {
		hij, ok := w.(http.Hijacker)
		if !ok {
			tools.Log.Println("server does not support Hijacker")
			return
		}
		src, _, err := hij.Hijack()
		if err != nil {
			tools.Log.Println("hijack error", err.Error())
			return
		}
		isSsl := strings.HasSuffix(r.URL.Host, ":443")
		if p.https != nil && p.d.HasReverse(r.URL.Host) || (isSsl && p.d.HasReverse(r.URL.Host[:len(r.URL.Host)-4])) {
			send200(src)
			p.l.connChan <- src
			return
		}

		defer src.Close()
		ctxVal := &ContextVal{
			Req:  r,
			Conn: src,
			Rpy:  true,
		}
		conn, err := p.Tr.DialContext(context.WithValue(r.Context(), CtxRequest, ctxVal), "tcp", r.URL.Host)
		if err != nil {
			tools.Log.Println("connect error", r.URL.Host)
			_ = src.Close()
			return
		}
		defer conn.Close()
		if ctxVal.Rpy {
			send200(src)
		}

		if isSsl {
			Free443(src, conn)
		}

		tools.CopyAndWait(src, conn)
	} else {
		p.Proxy.ServeHTTP(w, r)
	}
}

func Free443(src, tar net.Conn) {
	_ = src.SetReadDeadline(time.Now().Add(1 * time.Second))
	buf := []byte{0}
	n, _ := src.Read(buf)
	if n != 0 {
		_, _ = tar.Write(buf[:n])
	}
	_ = src.SetReadDeadline(time.Time{})
}
